/* * JAAS Jetty Crowd * Copyright (C) 2014 Issa Gorissen * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package be.greenhand.jaas.jetty; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.rmi.RemoteException; import java.security.Principal; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import javax.ws.rs.core.MediaType; import org.eclipse.jetty.plus.jaas.JAASPrincipal; import org.eclipse.jetty.plus.jaas.JAASRole; import org.eclipse.jetty.plus.jaas.callback.ObjectCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import be.greenhand.jaas.jetty.jaxb.AuthenticatePost; import be.greenhand.jaas.jetty.jaxb.GroupResponse; import be.greenhand.jaas.jetty.jaxb.GroupsResponse; import be.greenhand.jaas.jetty.jaxb.UserResponse; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.client.apache.ApacheHttpClient; import com.sun.jersey.client.apache.config.ApacheHttpClientConfig; import com.sun.jersey.client.apache.config.ApacheHttpClientState; import com.sun.jersey.client.apache.config.DefaultApacheHttpClientConfig; public class CrowdLoginModule implements LoginModule { private static final Logger LOG = LoggerFactory.getLogger(CrowdLoginModule.class); private static final String APP_NAME = "applicationName"; private static final String APP_PASS = "applicationPassword"; private static final String CROWD_SERVER_URL = "crowdServerUrl"; /* in secs - default 5 */ private static final String HTTP_PROXY_HOST = "httpProxyHost"; private static final String HTTP_PROXY_PORT = "httpProxyPort"; private static final String HTTP_PROXY_USER = "httpProxyUsername"; private static final String HTTP_PROXY_PASS = "httpProxyPassword"; /* default 20 */ private static final String HTTP_MAX_CONNECTIONS = "httpMaxConnections"; /* in millisecs - default 5000 */ private static final String HTTP_TIMEOUT = "httpTimeout"; private static final String SUPPLEMENTAL_ROLES = "supplementalRoles"; private Subject subject; private CallbackHandler callbackHandler; private Map<String, ?> options; /* REST client */ private Client client; private URI crowdServer; /* state machine */ private boolean authenticated = false; private boolean commited = false; private UserResponse currentUser = null; private Principal userPrincipal = null; private Set<JAASRole> rolePrincipals = null; public CrowdLoginModule() {} /** * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject, javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map) */ @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { this.subject = subject; this.callbackHandler = callbackHandler; this.options = options; try { restClientInit(); } catch (URISyntaxException ue) { throw new RuntimeException("Problem with JAAS config for Crowd", ue); } } /** * @see javax.security.auth.spi.LoginModule#login() */ @Override public boolean login() throws LoginException { try { if (callbackHandler == null) { throw new LoginException("No callback handler"); } Callback[] callbacks = configureCallbacks(); callbackHandler.handle(callbacks); String username = ((NameCallback) callbacks[0]).getName(); String password = (String) ((ObjectCallback) callbacks[1]).getObject(); if (username == null || password == null) { authenticated = false; } authenticate(username, password); authenticated = true; } catch (Exception e) { LOG.error("login()", e); throw new FailedLoginException(e.getMessage()); } return authenticated; } /** * @see javax.security.auth.spi.LoginModule#commit() */ @Override public boolean commit() throws LoginException { if (!authenticated) { resetStateData(); return false; } try { // create Jetty JAASPrincipal userPrincipal = new JAASPrincipal(currentUser.name); // create Jetty JAASRole rolePrincipals = getUserGroups(currentUser.name); // update Subject subject.getPrincipals().add(userPrincipal); subject.getPrincipals().addAll(rolePrincipals); if (LOG.isDebugEnabled()) { LOG.debug(subject.toString()); } commited = true; return commited; } catch (Exception e) { LOG.error("JAAS commit() failure", e); resetStateData(); throw new LoginException(e.getMessage()); } } /** * @see javax.security.auth.spi.LoginModule#abort() */ @Override public boolean abort() throws LoginException { if (LOG.isDebugEnabled()) { LOG.debug(String.format("abort called - previous login state: [%b]", authenticated)); } try { if (!authenticated) { return false; } else { resetStateData(); return true; } } catch (Exception e) { LOG.error("JAAS abort() failure", e); throw new LoginException(e.getMessage()); } } /** * @see javax.security.auth.spi.LoginModule#logout() */ @Override public boolean logout() throws LoginException { // remove JAASRole from subject subject.getPrincipals().remove(userPrincipal); // remove JAASPrincipal from subject subject.getPrincipals().removeAll(rolePrincipals); // reset state variables resetStateData(); return true; } private Callback[] configureCallbacks() { Callback[] callbacks = new Callback[2]; callbacks[0] = new NameCallback("Enter user name"); callbacks[1] = new ObjectCallback(); return callbacks; } private void resetStateData() { authenticated = false; commited = false; currentUser = null; userPrincipal = null; rolePrincipals = null; } /** * Prepares the REST client * @throws URISyntaxException */ private void restClientInit() throws URISyntaxException { DefaultApacheHttpClientConfig clientConfig = new DefaultApacheHttpClientConfig(); clientConfig.getProperties().put(ApacheHttpClientConfig.PROPERTY_HANDLE_COOKIES, Boolean.TRUE); clientConfig.getProperties().put(ApacheHttpClientConfig.PROPERTY_PREEMPTIVE_AUTHENTICATION, Boolean.TRUE); clientConfig.getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, getHttpTimeout()); clientConfig.getProperties().put(ClientConfig.PROPERTY_READ_TIMEOUT, getHttpTimeout()); clientConfig.getProperties().put(ClientConfig.PROPERTY_THREADPOOL_SIZE, getHttpMaxConnections()); crowdServer = new URI(getCrowdServerUrl()).resolve("rest/usermanagement/1/"); ApacheHttpClientState httpState = new ApacheHttpClientState(); httpState.setCredentials(null, crowdServer.getHost(), crowdServer.getPort(), getApplicationName(), getApplicationPassword()); if (getHttpProxyHost().trim().length() > 0 && getHttpProxyPort() > 0) { clientConfig.getProperties().put(ApacheHttpClientConfig.PROPERTY_PROXY_URI, getHttpProxyHost() + ':' + getHttpProxyPort()); if (getHttpProxyUsername() != null && getHttpProxyPassword() != null) { httpState.setProxyCredentials(null, getHttpProxyHost(), getHttpProxyPort(), getHttpProxyUsername(), getHttpProxyPassword()); } } clientConfig.getProperties().put(ApacheHttpClientConfig.PROPERTY_HTTP_STATE, httpState); if (LOG.isDebugEnabled()) { LOG.debug("HTTP Client config"); LOG.debug(getCrowdServerUrl()); LOG.debug(crowdServer.toString()); LOG.debug("PROPERTY_THREADPOOL_SIZE:" + clientConfig.getProperty(ClientConfig.PROPERTY_THREADPOOL_SIZE)); LOG.debug("PROPERTY_READ_TIMEOUT:" + clientConfig.getProperty(ClientConfig.PROPERTY_READ_TIMEOUT)); LOG.debug("PROPERTY_CONNECT_TIMEOUT:" + clientConfig.getProperty(ClientConfig.PROPERTY_CONNECT_TIMEOUT)); LOG.debug("PROPERTY_PROXY_URI:" + clientConfig.getProperty(ApacheHttpClientConfig.PROPERTY_PROXY_URI)); LOG.debug("Crowd application name:" + getApplicationName()); } client = ApacheHttpClient.create(clientConfig); } /** * Makes REST call toward Crowd to authenticate user */ private void authenticate(String username, String pass) throws RemoteException, UnsupportedEncodingException { if (LOG.isDebugEnabled()) LOG.debug("authentication attempt for '" + String.valueOf(username) + "'"); WebResource r = client.resource(crowdServer.resolve("authentication?username=" + URLEncoder.encode(username, "UTF-8"))); AuthenticatePost rBody = new AuthenticatePost(); rBody.value = pass; UserResponse response = r.accept(MediaType.APPLICATION_XML_TYPE).post(UserResponse.class, rBody); if (LOG.isDebugEnabled()) LOG.debug(response.toString()); LOG.info("authentication made for '" + String.valueOf(username) + "'"); currentUser = response; } /** * Makes REST call to Crowd to get user's groups * @param username * @return Set<JAASRole> * @throws RemoteException * @throws UnsupportedEncodingException */ private Set<JAASRole> getUserGroups(String username) throws RemoteException, UnsupportedEncodingException { if (LOG.isDebugEnabled()) LOG.debug("get groups for '" + String.valueOf(username) + "'"); WebResource r = client.resource(crowdServer.resolve("user/group/nested?username=" + URLEncoder.encode(username, "UTF-8"))); GroupsResponse response = r.get(GroupsResponse.class); if (LOG.isDebugEnabled()) LOG.debug(response.toString()); Set<JAASRole> results = new HashSet<JAASRole>(); for (GroupResponse group : response.group) { // check if group is active r = client.resource(crowdServer.resolve("group?groupname=" + URLEncoder.encode(group.name, "UTF-8"))); GroupResponse groupResponse = r.get(GroupResponse.class); if (groupResponse.active) { results.add(new JAASRole(group.name)); if (LOG.isDebugEnabled()) LOG.debug("Adding Group: " + group.name); } } // Add the specified supplemental roles to the user results.addAll(getSupplementalRoles()); return results; } private Integer getHttpTimeout() { int defaultVal = 5000; Object object = options.get(HTTP_TIMEOUT); if (object != null) { String value = (String) object; try { return Integer.valueOf(value); } catch (NumberFormatException nfe) { LOG.warn("JAAS config for Crowd http timeout must be a number in millisecs"); return new Integer(defaultVal); } } else { // default - 5000 msecs return new Integer(defaultVal); } } private Integer getHttpMaxConnections() { int defaultVal = 20; Object object = options.get(HTTP_MAX_CONNECTIONS); if (object != null) { String value = (String) object; try { return Integer.valueOf(value); } catch (NumberFormatException nfe) { LOG.warn("JAAS config for Crowd http max connections must be a number"); // default - 20 return new Integer(defaultVal); } } else { // default - 20 return new Integer(defaultVal); } } private String getCrowdServerUrl() { Object object = options.get(CROWD_SERVER_URL); if (object != null) { String baseUrl = (String) object; if (!baseUrl.endsWith("/")) { baseUrl += "/"; } return baseUrl; } else { throw new RuntimeException("JAAS config for Crowd is missing the crowd url which should look like https://a.domain.com/crowd/"); } } private Set<JAASRole> getSupplementalRoles() { Set rolesRet = new HashSet(); String defaultValue = "user"; Object object = options.get(SUPPLEMENTAL_ROLES); if (object != null) { String[] roles = ((String) object).split(","); for(String role : roles) { if (LOG.isDebugEnabled()) LOG.debug("Adding suplemental role: " + role); rolesRet.add(new JAASRole(role)); } } else { if (LOG.isDebugEnabled()) LOG.debug("Adding default suplemental role: " + defaultValue); rolesRet.add(new JAASRole(defaultValue)); } return rolesRet; } private String getApplicationName() { Object object = options.get(APP_NAME); if (object != null) { return (String) object; } else { throw new RuntimeException("JAAS config for Crowd is missing the crowd application name (app username in crowd)"); } } private String getApplicationPassword() { Object object = options.get(APP_PASS); if (object != null) { return (String) object; } else { throw new RuntimeException("JAAS config for Crowd is missing the crowd application password (app password in crowd)"); } } private String getHttpProxyHost() { Object object = options.get(HTTP_PROXY_HOST); if (object != null) { return (String) object; } else { return ""; } } private String getHttpProxyUsername() { return (String) options.get(HTTP_PROXY_USER); } private String getHttpProxyPassword() { return (String) options.get(HTTP_PROXY_PASS); } private int getHttpProxyPort() { Object object = options.get(HTTP_PROXY_PORT); if (object != null) { String value = (String) object; try { return Integer.valueOf(value).intValue(); } catch (NumberFormatException nfe) { throw new RuntimeException("JAAS config for Crowd http proxy port must be a number", nfe); } } else { return -1; } } }